ysifandomcom-20200214-history
Library:YSI\y als
ALS (the Advanced Library System) is a well established standard for hooking callbacks in multiple includes transparently, using a well known set of pre-processor directives to mitigate any collisions. y_als is a core module in YSI, though it is also quite an internal one in that it is only really used by YSI. However, it does provide a number of useful features that can make working with callbacks (especially large numbers of them) simpler. This library was originally developed to simplify the development of ALS hooks, but the actual code used within ALS has advanced to such a point that this library is now largely surplus to requirements. However, it still allows you to do things "for all" callbacks simply without having to worry about parameters/calling conventions/specifiers etc. Some of the "improvements" here may seem minimal, but in a large library like YSI which can hook any given callback multiple times (and it does hook them all in at least one place), it makes a big time saving. Errors For a list of possible errors when using this library, see . Example Standard ALS hook: Using y_als: Note that this is not a fair comparison - the first code block will not work in a filterscript, the YSI version will. Explanation Assuming you already understand ALS, this will go through every line of the second version in turn. In the original version "Hook_OnPlayerConnect" was used, this is the same required prefix as there, but defined only once in a file to reduce typos mistakes (which are not detectable in strings). Just include the ALS code. Frankly you don't even need this if you've already included at least one YSI file (any of them - y_als is one of the globaly incldued "core" files because it makes hooks simpler. This is similar to the "gHasOPC" variable in the original code, providing memory in which to store wether or not another callback exists. The difference between this and the original version is that this is designed to efficiently handle all the default SA:MP callbacks (plus a few YSI ones) - currently it uses only 2 cells for them all. This is a YSI abstraction, it is either "OnGameModeInit" or "OnFilterScriptInit", depending on what the script is running as - basically it is called at the start of a script regardless of what the script is. What's more, you no longer need the "FILTERSCRIPT" definition for this to work - it figures it all out at runtime and so always works even if a user gets something wrong. This is frankly very useful for writing libraries! This checks if there are any more of this callback that need chaining (as in the standard version of ALS). There is a slight limitation of the system in that you must do this even for callbacks that are only ever called once (such as "OnScriptInit"). Note the lack of "On" in this text for simplicitly. This checks to see if "Hook_PlayerConnect" exists, just the same as the "funcidx" code did in the original ALS version. If "Hook_ScriptInit" exists, call it and return the result. If it doesn't then just return the default value (1 for "OnScriptInit"). "OnScriptInit" is a YSI callback, which were all designed to be "ALS protected", as in you don't need the complex ALS macros to hook them, you just undefine the old version and define a new version. This renames the next version of the callback so that the calls can be chained (again as in ALS). Note again the lack of "On", this is to reduce symbol lengths for some longer callbacks. Forward the new version of the callback (i.e. "Hook_ScriptInit"). This is just a normal (ALS hooked) callback WITH A PARAMETER. This chains the current callback just as in the old version of ALS code, but with NO MENTION OF THE PARAMETER. Things like the "CallLocalFunction" specifier are abstracted - the system just knows to use "i" not "ssi" for example, it also doesn't chain the call if the other hooked function doesn't exist instead it returns the default return value. Again this is "1" for "OnPlayerConnect", but its "0" for "OnPlayerCommandText" and again the system knows this and does things properly. This is just the standard ALS hook macro set, which is unfortunately needed still. This line sees if another hook already exists. If another version of the hook already exists, remove the old hook renaming. If this is the first hook on "OnPlayerConnect", tell subsequent libraries (if any) that at least one hook now exists, and has defined a pre-processor symbol called "OnPlayerConnect". Create a pre-processor symbol called "OnPlayerConnect". This may be the same as a callback name, but the two symbol sets are entirely separate. Thus, if the pre-processor symbol with this name DOESN'T exist and we try "#undef" it, the compiler will give an error; however, if the pre-processor symbol with this name DOES exist and we don't "#undef" it, the compiler will give a warning - that's why the "_ALS_OnPlayerConnect" symbol is required. Again, this forwards the callback correctly, without needing to know anything about the parameters of the function and thus possibly getting them wrong. At the end of your file using "y_als", this is required for technical reasons (otherwise you can end up with multiple callbacks with the same name). Note that if you miss the "#define ALS_PREFIX" line but not the "#include " line, you will get the default prefix of "Mode", but you still need to "#undef" it. Justification So why does this exist in the first place, as on first glance it does seem quite pointless? The basic reason is that the code generation can be automated, in fact that's pretty much the only reason. All you need to change between "OnPlayerConnect" and "OnPlayerCommandText" is the function name for much of the code. You don't need to worry about parameters, specifiers ("i" or "is"), brackets (should there be a "[]" here or not), or tags. Plus it is a little quicker once you get used to it. This autmation becomes even more apparent when useing the library to extend its own functionality. Print Example To show some more advanced features of the library, let's develop a system to print out a callback and all its parameters. At first glance this seems easy: And yes, that is easy, until you want to do it for ALL callbacks using (say) regular expressions. This may not be something you ever do, but is something done in YSI with surprising regularity; not just printing callbacks, but just doing SOMETHING for ALL callbacks, in those cases it is better to let the compiler worry about tags, brackets, and strings, etc. For this we need to look in to the library a little deeper. Opening up y_als shows multiple lines like this: The "ALS_R_" line is very simply just the default return value for that callback (note again the lack of "On" in all these lines for symbol length limit reasons). Nothing more really needs to be said about that, it's "1" in all but two places (one of those being a YSI callback). The "ALS_DO_" line is a little more complex and will be broken down further: This defines the "ALS_DO_" line for the "OnDialogResponse" callback. This macro is actually an internal one. The value of the parameter "%0" will become another macro to get called. This calls the "%0" macro, passing all the data available about this callback. The "<>"s enclose callback information, the "()"s enclose the callback parameters. This is the name of the callback again (so that is gets passed to the macro, though I have just figured out another way of doing this so you don't need it twice, but since this is all internal data that's already written it doesn't really matter), followed by the specifier for "CallLocalFunction" and similar functions. Note no spaces. This is the first parameter, or more to the point this is not the last parameter. Because it is not the last, there are "more" to come and the parameter is marked as such. These are also not the last parameter so are prefixed by "more:" to indicate this fact. This IS the last parameter, so uses "end", it is also a string so uses "string". Although this example doesn't show them all, the full set (with examples from real callbacks) is: * more:playerid - A parameter called "playerid" that isn't the last in the list. * Float:fRotX - A parameter called "fRotX" that isn't the last in the list and has a tag of "Float". * tag:Text:none - A parameter called "none" that isn't the last in the list and has a different tag (but is still an integer) (never used). * string:password[] - A parameter called "password" that isn't the last in the list and is a string. * end:playerid - A parameter called "playerid" that is the last in the list. * end_Float:fZ - A parameter called "fZ" that is the last in the list and has a tag of "Float". * end_tag:PlayerText:playertextid - A parameter called "playertextid" that is the last in the list and has a tag of "PlayerText" (but is still an integer). * end_string:inputtext[] - A parameter called "inputtext" that is the last in the list and is a string. * none: - A callback with no parameters. Again note the lack of spaces. Because these are internal macros that users never need to see (or understand, this was really a waste of time for you to read :p), the format is tightly controlled to make other code simpler. Speaking of other code, the "print" macro will be an example of a "%0" macro above, so will have the following format: Here "%0" is the callback's name, "%1" is the callback's specifier, and "%2" is a DESCRIPTION of the callback's parameters. Thus part of the code becomes: Now those two lines LOOK similar, but they're NOT! The first one will just print "OnPlayerConnect" (for example) all the time. The second one will print the current callback's name AFTER ALS substitutions have been done, so it could print "OnPlayerConnect" if it is the first callback in a chain or something else such as "Mode_PlayerConnect" depending on what came before. Which one you want is up to you, but this discussion will stick to the second one for now as it can make debugging slightly simpler. Now the format specifier (for example "%d %s") is required. However, it turns out that generating that data from the parameters themselves (due to the descriptions) is actually easier to do: Using that, and a variant on the inbuilt "ALS_RS_" macro (which converts parameters to their "call" equivalents; unlike the "ALS_KS_" macro, which converts them to their "function declaration" equivalents) results in: Here I should point out the extra ")", which is detected and removed by the "end:" and "none:" "ALS_PS_" macros. And a final piece of boilerplate to make the macro easy to use: Results in the following code to fully debug the "OnPlayerConnect" callback with parameters: And have it just work. Same for any other callback. For reference, this generates the following code using "-l": And in "OnDialogResponse": Here you can see the effect of the ALS expansion discussed earlier to give the final names, not the typed names. You can also see all the parameters nicely (and safely) printed correctly. There is also the "q" macro: This is a macro that is used so: Here "%1" is obviously the function specifier, followed by a comma, then followed by the parameters. However, if there are no parameters then the comma should not be there and the special "q" specifier (which is ignored normally) removes it. Unfortunately as specifiers were not used in the print example, the "ALS_R2_" macro had to do a similar thing.